/*
 * marketsim-eclipsetrader
 *
 * Copyright (C) 2008-2011 Steve Phelps and John Cartlidge
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 */
package victria.marketsim.core;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import org.apache.log4j.Logger;

import victria.marketsim.event.MarketEvent;
import victria.marketsim.event.MarketEventListener;
import victria.marketsim.event.MarketOpenedEvent;
import victria.marketsim.event.NewOrderEvent;
import victria.marketsim.event.OrderCancelledEvent;
import victria.marketsim.event.TransactionExecutedEvent;

public class Market implements Serializable, MarketService {

	private HashSet<Order> matchedOrders;
	
	protected LinkedList<Transaction> pendingTransactions;
	
	protected LinkedList<Order> pendingInsertions;
	
	protected List<MarketEventListener> listeners;
	
	protected MarketState marketState;
	
	/**
	 * The traders participating in this market.
	 */
	protected HashSet<Object> traders;
	
	protected float lastTransactionPrice;
	
	static Logger logger = Logger.getLogger(Market.class);
	
	protected static IdCounter orderIdCounter = new IdCounter();
	protected static IdCounter traderIdCounter = new IdCounter();
	
	public Market() {
		listeners = new LinkedList<MarketEventListener>();
		matchedOrders = new HashSet<Order>();
		pendingTransactions = new LinkedList<Transaction>();
		pendingInsertions = new LinkedList<Order>();
		marketState = new MarketState();
		traders = new HashSet();
	}
	
	public void add(Order order) {
		marketState.add(order);
	}


	public boolean contains(Order order) {
		return marketState.contains(order);
	}


	public List<Order> getAsks() {
		return marketState.getAsks();
	}


	public List<Order> getBids() {
		return marketState.getBids();
	}


	public List<Order> getVisibleAsks() {
		return marketState.getVisibleAsks();
	}


	public List<Order> getVisibleBids() {
		return marketState.getVisibleBids();
	}

	public boolean isEmpty() {
		return marketState.isEmpty();
	}

	public boolean remove(Order order) {
		return marketState.remove(order);
	}

	public synchronized void registerTrader(Object trader) {
		traders.add(trader);
	}
	
	public void fireEvent(MarketEvent event) {
		try {
			logger.debug("Firing event " + event);
			if (listeners != null) {
				for(MarketEventListener listener : listeners) {
					logger.debug("Sending " + event + " to " + listener);
					listener.eventOccurred(event);
				}
			}
		} catch (Exception e) {
			logger.error(e);
		}
	}
	
	public synchronized void addListener(MarketEventListener listener) {
		logger.debug("Adding listener " + listener);
		if (! listeners.contains(listener)) {
			listeners.add(listener);
		}
	}
	
	public synchronized void removeListener(MarketEventListener listener) {
		logger.debug("Adding listener " + listener);
		listeners.remove(listener);
	}
	
	public synchronized long placeOrder(Order order) {
		logger.info("New order placed: " + order.toPrettyString());
		order.setDate(new Date());
		synchronized (pendingTransactions) {
			removeExpiredOrders();
			order.setId(allocateOrderId());			
			logger.debug("order = " + order);
			traders.add(order.getTrader());
			add(order);
			performClearing();
		}
		fireEvent(new NewOrderEvent(order));
		return order.getId();
	}
	
	public long replaceOrder(long id, Order order) {
		long result = -1;
		boolean success = false;
		Order existing = null;
		existing = getOrder(id);
		if (existing != null) {
			logger.debug("Removing existing order " + existing);
			success = remove(existing);
		}
		result = placeOrder(order);
		if (success) {
			fireEvent(new OrderCancelledEvent(existing));
		}
		return result;
	}
	
	public boolean cancelOrder(long id) {
		logger.debug("Cancelling order id " + id);
		boolean success = false;
		Order existing = null;
		synchronized(pendingTransactions) {
			existing = getOrder(id);
			if (existing != null) {		
				logger.debug("Cancelling existing order " + existing);
				success = remove(existing);
			}
		}	
		if (success) {
			fireEvent(new OrderCancelledEvent(existing));
		}
		return success;
	}
	
	public boolean cancelOrders(Object trader) {
		logger.debug("Cancelling all outstanding orders for trader " + trader);
		boolean success = false;
		synchronized(pendingTransactions) {
			List<Order> outstanding = new ArrayList<Order>(getOrders(trader));
			for(Order order : outstanding) {
				success = remove(order);
				if (!success) {
					break;
				} else {
					fireEvent(new OrderCancelledEvent(order));
				}
			}
		}
		return success;
	}
	
	protected synchronized void createTransactions(Order bid, Order ask) {
		
		float transactionPrice = bid.getTransactionPrice(ask);
		int maximumSingleTransactionSize = bid.getMaximumSingleTransactionSize(ask);
		
		//repeatedly execute at maximumSingleTransactionSize until fulfilledQty is reached
		while(bid.getFulfilledQuantity(ask) > 0) {
			int filled;
			Order transactedAsk, transactedBid;
			Transaction transaction;
			int remaining = bid.getFulfilledQuantity(ask);
			if (remaining > maximumSingleTransactionSize) {
				filled = maximumSingleTransactionSize;			
			} else {
				filled = remaining;
			}
			transactedAsk = createOrder(ask, filled);
			transactedBid = createOrder(bid, filled);
			remaining -= filled;
			bid.increaseFilledQuantity(filled);
			ask.increaseFilledQuantity(filled);
			transaction = new Transaction(transactedBid, transactedAsk, maximumSingleTransactionSize, transactionPrice);
			logger.info("New transaction: " + transaction.toPrettyString());
			pendingTransactions.add(transaction);
		}
	}
	
	protected synchronized void updateOrderQuantities(Order bid, Order ask) {
		
		Order pendingOrder = null;	
		int fulfilledQuantity = bid.getFulfilledQuantity(ask);
		
		if (bid.getQuantity() < ask.getQuantity()) {
			pendingOrder = createOrder(ask, ask.getQuantity() - fulfilledQuantity);
			pendingInsertions.add(pendingOrder);
			
		} else if (ask.getQuantity() < bid.getQuantity()) {
			pendingOrder= createOrder(bid, bid.getQuantity() - fulfilledQuantity);
			pendingInsertions.add(pendingOrder);
		} else {
			//do nothing - both orders execute fully
		}
	}
	
	protected synchronized void cross(Order bid, Order ask) {

		logger.info("Crossing " + bid.toPrettyString() + " with "
				+ ask.toPrettyString());
		assert bid.isBid();
		assert ask.isAsk();
		
		createTransactions(bid, ask);
		updateOrderQuantities(bid, ask);
	}
	
	public Order getOrder(long id) {
		return marketState.getOrder(id);
	}


	public List<Order> getOrders(Object trader) {
		return marketState.getOrders(trader);
	}


	public boolean isOutstanding(long id) {
		return marketState.isOutstanding(id);
	}


	public void executePendingTransactions() {
		for(Transaction transaction : pendingTransactions) {
			fireEvent(new TransactionExecutedEvent(transaction));
			lastTransactionPrice = transaction.getTransactionPrice();
		}
	}
	
	public  boolean handlePendingInsertions() {		
		for (Order order : pendingInsertions) {
			add(order);
		}
		return !pendingInsertions.isEmpty();
	}
	
	public void removeMatchedOrders() {
		for (Order order : matchedOrders) {
			remove(order);
		}
	}
	
	public void removeExpiredOrders() {
		logger.debug("Removing expired orders..");
		LinkedList<Order> expiredOrders = new LinkedList<Order>();
		for (Order order : marketState.getAllOrders()) {
			logger.debug("Checking " + order);
			if (order.isExpired()) {
				logger.debug("Order is expired");
				expiredOrders.add(order);
			}
		}
		for (Order expired : expiredOrders) {
			logger.debug("Removing expired order " + expired);
			remove(expired);
		}
		logger.debug("done.");
	}
	
	public void performClearing() {
		logger.info("Clearing the market.. ");
		do {
			matchedOrders.clear();
			pendingInsertions.clear();
			pendingTransactions.clear();
			
			for(Order bid : getBids()) {
				for(Order ask : getAsks()) {
					if (bid.matches(ask)) {
						cross(bid, ask);
						matchedOrders.add(bid);
						matchedOrders.add(ask);
					}
				}
			}
			
			removeMatchedOrders();
			executePendingTransactions();
		} while (handlePendingInsertions());
		logger.info("Market clearing complete.");
	}

	
	public synchronized int allocateOrderId() {
		return orderIdCounter.getId();
	}
	
	public synchronized Object allocateUniqueTrader() {
		Integer id;
		Object trader;
		do {
			id = traderIdCounter.getId();
			trader = "trader#" + id;
		} while (traders.contains(trader));
		return trader;
	}
	
	protected Order createOrder() {
		Order result = new Order();
		result.setId(allocateOrderId());
		return result;
	}
	
	protected Order createOrder(Order existing, int quantity) {
		try {
			Order result = (Order) existing.clone();
			result.setId(allocateOrderId());
			result.setQuantity(quantity);
			return result;
		} catch (CloneNotSupportedException e) {
			throw new SimRuntimeException(e);
		}
	}
	
	public MarketState getMarketState() {
//		removeExpiredOrders();
		return marketState;
	}
	
	public void setMarketState(MarketState marketState) {
		this.marketState = marketState;
	}

	public List<MarketEventListener> getListeners() {
		return listeners;
	}

	public void setListeners(List<MarketEventListener> listeners) {
		this.listeners = listeners;
	}
	
	public boolean isRunning() {
		return true;
	}
	
	public void openMarket() {
		fireEvent(new MarketOpenedEvent(this));
	}
	
	public long currentTimeMillis() {
		return System.currentTimeMillis();
	}
	
	
	public static class IdCounter {
		
		public int id = 1;
		
		public int getId() {
			synchronized(this) {
				return id++;
			}
		}
		
	}

	@Override
	public double getLastTransactionPrice() {
		return lastTransactionPrice;
	}

}
